// Copyright © 2013-2017 Esko Luontola and other Retrolambda contributors
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0

package net.orfjackal.retrolambda.lambdas;

import org.objectweb.asm.*;

import java.lang.invoke.*;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.concurrent.*;

public class LambdaReifier {

    // These globals are used for communicating with the Java agent which
    // is spying on the LambdaMetafactory's dynamically generated bytecode.
    // We expect only one class being processed at a time, so it should
    // be an error if these collections contain more than one element.
    private static final BlockingDeque<Handle> currentLambdaImplMethod = new LinkedBlockingDeque<>(1);
    private static final BlockingDeque<Handle> currentLambdaAccessMethod = new LinkedBlockingDeque<>(1);
    private static final BlockingDeque<Class<?>> currentInvoker = new LinkedBlockingDeque<>(1);
    private static final BlockingDeque<Type> currentInvokedType = new LinkedBlockingDeque<>(1);
    private static final BlockingDeque<String> currentLambdaClass = new LinkedBlockingDeque<>(1);
    private static final BlockingDeque<EnclosingClass> currentEnclosingClass = new LinkedBlockingDeque<>(1);

    public static LambdaFactoryMethod reifyLambdaClass(EnclosingClass enclosingClass, Handle lambdaImplMethod, Handle lambdaAccessMethod,
                                                       Class<?> invoker, String invokedName, Type invokedType, Handle bsm, Object[] bsmArgs) {
        try {
            setLambdaImplMethod(lambdaImplMethod);
            setLambdaAccessMethod(lambdaAccessMethod);
            setInvoker(invoker);
            setInvokedType(invokedType);
            setEnclosingClass(enclosingClass);

            // Causes the lambda class to be loaded. Retrolambda's Java agent
            // will detect it, save it to a file and tell us (via the globals
            // in this class) that what the name of the lambda class was.
            callBootstrapMethod(invoker, invokedName, invokedType, bsm, bsmArgs);

            return getLambdaFactoryMethod();

        } catch (Throwable t) {
            throw new RuntimeException("Failed to backport lambda or method reference: " + lambdaImplMethod, t);
        } finally {
            resetGlobals();
        }
    }

    private static void setLambdaImplMethod(Handle lambdaImplMethod) {
        currentLambdaImplMethod.push(lambdaImplMethod);
    }

    private static void setLambdaAccessMethod(Handle lambdaAccessMethod) {
        currentLambdaAccessMethod.push(lambdaAccessMethod);
    }

    private static void setInvoker(Class<?> lambdaInvoker) {
        currentInvoker.push(lambdaInvoker);
    }

    private static void setInvokedType(Type invokedType) {
        currentInvokedType.push(invokedType);
    }

    public static void setLambdaClass(String lambdaClass) {
        currentLambdaClass.push(lambdaClass);
    }

    public static void setEnclosingClass(EnclosingClass enclosingClass) {
        currentEnclosingClass.push(enclosingClass);
    }

    public static boolean isLambdaClassToReify(String className) {
        Class<?> invoker = currentInvoker.peekFirst();
        return invoker != null
                && className.startsWith(Type.getInternalName(invoker))
                && LambdaNaming.LAMBDA_CLASS.matcher(className).matches();
    }

    public static Handle getLambdaImplMethod() {
        return currentLambdaImplMethod.getFirst();
    }

    public static Handle getLambdaAccessMethod() {
        return currentLambdaAccessMethod.getFirst();
    }

    public static EnclosingClass getEnclosingClass() {
        return currentEnclosingClass.getFirst();
    }

    public static LambdaFactoryMethod getLambdaFactoryMethod() {
        String lambdaClass = currentLambdaClass.getFirst();
        Type invokedType = currentInvokedType.getFirst();
        return new LambdaFactoryMethod(lambdaClass, invokedType);
    }

    private static void resetGlobals() {
        currentLambdaImplMethod.clear();
        currentLambdaAccessMethod.clear();
        currentInvoker.clear();
        currentInvokedType.clear();
        currentLambdaClass.clear();
        currentEnclosingClass.clear();
    }

    private static CallSite callBootstrapMethod(Class<?> invoker, String invokedName, Type invokedType, Handle bsm, Object[] bsmArgs) throws Throwable {
        ClassLoader cl = invoker.getClassLoader();
        MethodHandles.Lookup caller = getLookup(invoker);

        List<Object> args = new ArrayList<>();
        args.add(caller);
        args.add(invokedName);
        args.add(Types.toMethodType(invokedType, cl));
        for (Object arg : bsmArgs) {
            args.add(Types.asmToJdkType(arg, cl, caller));
        }

        MethodHandle bootstrapMethod = Types.toMethodHandle(bsm, cl, caller);
        return (CallSite) bootstrapMethod.invokeWithArguments(args);
    }

    private static MethodHandles.Lookup getLookup(Class<?> targetClass) throws Exception {
        Constructor<MethodHandles.Lookup> ctor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class);
        ctor.setAccessible(true);
        return ctor.newInstance(targetClass);
    }
}
